/*
* DragSortListView.
*
* A subclass of the Android ListView component that enables drag
* and drop re-ordering of list items.
*
* Copyright 2012 Carl Bauer
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mobeta.android.dslv;
import net.nurik.roman.dashclock.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.drawable.Drawable;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Point;
import android.os.Environment;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.*;
import android.widget.*;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
/**
* ListView subclass that mediates drag and drop resorting of items.
*
*
* @author heycosmo
*
*/
public class DragSortListView extends ListView {
/**
* The View that floats above the ListView and represents
* the dragged item.
*/
private View mFloatView;
/**
* The float View location. First based on touch location
* and given deltaX and deltaY. Then restricted by callback
* to FloatViewManager.onDragFloatView(). Finally restricted
* by bounds of DSLV.
*/
private Point mFloatLoc = new Point();
private Point mTouchLoc = new Point();
/**
* The middle (in the y-direction) of the floating View.
*/
private int mFloatViewMid;
/**
* Flag to make sure float View isn't measured twice
*/
private boolean mFloatViewOnMeasured = false;
/**
* Watch the Adapter for data changes. Cancel a drag if
* coincident with a change.
*/
private DataSetObserver mObserver;
/**
* Transparency for the floating View (XML attribute).
*/
private float mFloatAlpha = 1.0f;
private float mCurrFloatAlpha = 1.0f;
/**
* While drag-sorting, the current position of the floating
* View. If dropped, the dragged item will land in this position.
*/
private int mFloatPos;
/**
* The first expanded ListView position that helps represent
* the drop slot tracking the floating View.
*/
private int mFirstExpPos;
/**
* The second expanded ListView position that helps represent
* the drop slot tracking the floating View. This can equal
* mFirstExpPos if there is no slide shuffle occurring; otherwise
* it is equal to mFirstExpPos + 1.
*/
private int mSecondExpPos;
/**
* Flag set if slide shuffling is enabled.
*/
private boolean mAnimate = false;
/**
* The user dragged from this position.
*/
private int mSrcPos;
/**
* Offset (in x) within the dragged item at which the user
* picked it up (or first touched down with the digitalis).
*/
private int mDragDeltaX;
/**
* Offset (in y) within the dragged item at which the user
* picked it up (or first touched down with the digitalis).
*/
private int mDragDeltaY;
/**
* The difference (in x) between screen coordinates and coordinates
* in this view.
*/
private int mOffsetX;
/**
* The difference (in y) between screen coordinates and coordinates
* in this view.
*/
private int mOffsetY;
/**
* A listener that receives callbacks whenever the floating View
* hovers over a new position.
*/
private DragListener mDragListener;
/**
* A listener that receives a callback when the floating View
* is dropped.
*/
private DropListener mDropListener;
/**
* A listener that receives a callback when the floating View
* (or more precisely the originally dragged item) is removed
* by one of the provided gestures.
*/
private RemoveListener mRemoveListener;
/**
* Enable/Disable item dragging
*/
private boolean mDragEnabled = true;
/**
* Drag state enum.
*/
private final static int IDLE = 0;
private final static int REMOVING = 1;
private final static int DROPPING = 2;
private final static int STOPPED = 3;
private final static int DRAGGING = 4;
private int mDragState = IDLE;
/**
* Height in pixels to which the originally dragged item
* is collapsed during a drag-sort. Currently, this value
* must be greater than zero.
*/
private int mItemHeightCollapsed = 1;
/**
* Height of the floating View. Stored for the purpose of
* providing the tracking drop slot.
*/
private int mFloatViewHeight;
/**
* Convenience member. See above.
*/
private int mFloatViewHeightHalf;
/**
* Save the given width spec for use in measuring children
*/
private int mWidthMeasureSpec = 0;
/**
* Sample Views ultimately used for calculating the height
* of ListView items that are off-screen.
*/
private View[] mSampleViewTypes = new View[1];
/**
* Drag-scroll encapsulator!
*/
private DragScroller mDragScroller;
/**
* Determines the start of the upward drag-scroll region
* at the top of the ListView. Specified by a fraction
* of the ListView height, thus screen resolution agnostic.
*/
private float mDragUpScrollStartFrac = 1.0f / 3.0f;
/**
* Determines the start of the downward drag-scroll region
* at the bottom of the ListView. Specified by a fraction
* of the ListView height, thus screen resolution agnostic.
*/
private float mDragDownScrollStartFrac = 1.0f / 3.0f;
/**
* The following are calculated from the above fracs.
*/
private int mUpScrollStartY;
private int mDownScrollStartY;
private float mDownScrollStartYF;
private float mUpScrollStartYF;
/**
* Calculated from above above and current ListView height.
*/
private float mDragUpScrollHeight;
/**
* Calculated from above above and current ListView height.
*/
private float mDragDownScrollHeight;
/**
* Maximum drag-scroll speed in pixels per ms. Only used with
* default linear drag-scroll profile.
*/
private float mMaxScrollSpeed = 0.5f;
/**
* Defines the scroll speed during a drag-scroll. User can
* provide their own; this default is a simple linear profile
* where scroll speed increases linearly as the floating View
* nears the top/bottom of the ListView.
*/
private DragScrollProfile mScrollProfile = new DragScrollProfile() {
@Override
public float getSpeed(float w, long t) {
return mMaxScrollSpeed * w;
}
};
/**
* Current touch x.
*/
private int mX;
/**
* Current touch y.
*/
private int mY;
/**
* Last touch x.
*/
private int mLastX;
/**
* Last touch y.
*/
private int mLastY;
/**
* The touch y-coord at which drag started
*/
private int mDragStartY;
/**
* Drag flag bit. Floating View can move in the positive
* x direction.
*/
public final static int DRAG_POS_X = 0x1;
/**
* Drag flag bit. Floating View can move in the negative
* x direction.
*/
public final static int DRAG_NEG_X = 0x2;
/**
* Drag flag bit. Floating View can move in the positive
* y direction. This is subtle. What this actually means is
* that, if enabled, the floating View can be dragged below its starting
* position. Remove in favor of upper-bounding item position?
*/
public final static int DRAG_POS_Y = 0x4;
/**
* Drag flag bit. Floating View can move in the negative
* y direction. This is subtle. What this actually means is
* that the floating View can be dragged above its starting
* position. Remove in favor of lower-bounding item position?
*/
public final static int DRAG_NEG_Y = 0x8;
/**
* Flags that determine limits on the motion of the
* floating View. See flags above.
*/
private int mDragFlags = 0;
/**
* Last call to an on*TouchEvent was a call to
* onInterceptTouchEvent.
*/
private boolean mLastCallWasIntercept = false;
/**
* A touch event is in progress.
*/
private boolean mInTouchEvent = false;
/**
* Let the user customize the floating View.
*/
private FloatViewManager mFloatViewManager = null;
/**
* Given to ListView to cancel its action when a drag-sort
* begins.
*/
private MotionEvent mCancelEvent;
/**
* Enum telling where to cancel the ListView action when a
* drag-sort begins
*/
private static final int NO_CANCEL = 0;
private static final int ON_TOUCH_EVENT = 1;
private static final int ON_INTERCEPT_TOUCH_EVENT = 2;
/**
* Where to cancel the ListView action when a
* drag-sort begins
*/
private int mCancelMethod = NO_CANCEL;
/**
* Determines when a slide shuffle animation starts. That is,
* defines how close to the edge of the drop slot the floating
* View must be to initiate the slide.
*/
private float mSlideRegionFrac = 0.25f;
/**
* Number between 0 and 1 indicating the relative location of
* a sliding item (only used if drag-sort animations
* are turned on). Nearly 1 means the item is
* at the top of the slide region (nearly full blank item
* is directly below).
*/
private float mSlideFrac = 0.0f;
/**
* Wraps the user-provided ListAdapter. This is used to wrap each
* item View given by the user inside another View (currenly
* a RelativeLayout) which
* expands and collapses to simulate the item shuffling.
*/
private AdapterWrapper mAdapterWrapper;
/**
* Turn on custom debugger.
*/
private boolean mTrackDragSort = false;
/**
* Debugging class.
*/
private DragSortTracker mDragSortTracker;
/**
* Needed for adjusting item heights from within layoutChildren
*/
private boolean mBlockLayoutRequests = false;
/**
* Set to true when a down event happens during drag sort;
* for example, when drag finish animations are
* playing.
*/
private boolean mIgnoreTouchEvent = false;
/**
* Caches DragSortItemView child heights. Sometimes DSLV has to
* know the height of an offscreen item. Since ListView virtualizes
* these, DSLV must get the item from the ListAdapter to obtain
* its height. That process can be expensive, but often the same
* offscreen item will be requested many times in a row. Once an
* offscreen item height is calculated, we cache it in this guy.
* Actually, we cache the height of the child of the
* DragSortItemView since the item height changes often during a
* drag-sort.
*/
private static final int sCacheSize = 3;
private HeightCache mChildHeightCache = new HeightCache(sCacheSize);
private RemoveAnimator mRemoveAnimator;
private LiftAnimator mLiftAnimator;
private DropAnimator mDropAnimator;
public DragSortListView(Context context, AttributeSet attrs) {
super(context, attrs);
int defaultDuration = 150;
int removeAnimDuration = defaultDuration; //ms
int dropAnimDuration = defaultDuration; //ms
if (attrs != null) {
TypedArray a = getContext().obtainStyledAttributes(attrs,
R.styleable.DragSortListView, 0, 0);
mItemHeightCollapsed = Math.max(1, a.getDimensionPixelSize(
R.styleable.DragSortListView_collapsed_height, 1));
mTrackDragSort = a.getBoolean(
R.styleable.DragSortListView_track_drag_sort, false);
if (mTrackDragSort) {
mDragSortTracker = new DragSortTracker();
}
// alpha between 0 and 255, 0=transparent, 255=opaque
mFloatAlpha = a.getFloat(R.styleable.DragSortListView_float_alpha, mFloatAlpha);
mCurrFloatAlpha = mFloatAlpha;
mDragEnabled = a.getBoolean(R.styleable.DragSortListView_drag_enabled, mDragEnabled);
mSlideRegionFrac = Math.max(0.0f,
Math.min(1.0f, 1.0f - a.getFloat(
R.styleable.DragSortListView_slide_shuffle_speed,
0.75f)));
mAnimate = mSlideRegionFrac > 0.0f;
float frac = a.getFloat(
R.styleable.DragSortListView_drag_scroll_start,
mDragUpScrollStartFrac);
setDragScrollStart(frac);
mMaxScrollSpeed = a.getFloat(
R.styleable.DragSortListView_max_drag_scroll_speed,
mMaxScrollSpeed);
removeAnimDuration = a.getInt(
R.styleable.DragSortListView_remove_animation_duration,
removeAnimDuration);
dropAnimDuration = a.getInt(
R.styleable.DragSortListView_drop_animation_duration,
dropAnimDuration);
boolean useDefault = a.getBoolean(
R.styleable.DragSortListView_use_default_controller,
true);
if (useDefault) {
boolean removeEnabled = a.getBoolean(
R.styleable.DragSortListView_remove_enabled,
false);
int removeMode = a.getInt(
R.styleable.DragSortListView_remove_mode,
DragSortController.FLING_RIGHT_REMOVE);
boolean sortEnabled = a.getBoolean(
R.styleable.DragSortListView_sort_enabled,
true);
int dragInitMode = a.getInt(
R.styleable.DragSortListView_drag_start_mode,
DragSortController.ON_DOWN);
int dragHandleId = a.getResourceId(
R.styleable.DragSortListView_drag_handle_id,
0);
int clickRemoveId = a.getResourceId(
R.styleable.DragSortListView_click_remove_id,
0);
int bgColor = a.getColor(
R.styleable.DragSortListView_float_background_color,
Color.BLACK);
DragSortController controller = new DragSortController(
this, dragHandleId, dragInitMode, removeMode,
clickRemoveId);
controller.setRemoveEnabled(removeEnabled);
controller.setSortEnabled(sortEnabled);
controller.setBackgroundColor(bgColor);
mFloatViewManager = controller;
setOnTouchListener(controller);
}
a.recycle();
}
mDragScroller = new DragScroller();
float smoothness = 0.5f;
if (removeAnimDuration > 0) {
mRemoveAnimator = new RemoveAnimator(smoothness, removeAnimDuration);
}
//mLiftAnimator = new LiftAnimator(smoothness, 100);
if (dropAnimDuration > 0) {
mDropAnimator = new DropAnimator(smoothness, dropAnimDuration);
}
mCancelEvent = MotionEvent.obtain(0,0,MotionEvent.ACTION_CANCEL,0f,0f,0f,0f,0,0f,0f,0,0);
// construct the dataset observer
mObserver = new DataSetObserver() {
private void cancel() {
if (mDragState == DRAGGING) {
cancelDrag();
}
}
@Override
public void onChanged() {
cancel();
}
@Override
public void onInvalidated() {
cancel();
}
};
}
/**
* Usually called from a FloatViewManager. The float alpha
* will be reset to the xml-defined value every time a drag
* is stopped.
*/
public void setFloatAlpha(float alpha) {
mCurrFloatAlpha = alpha;
}
public float getFloatAlpha() {
return mCurrFloatAlpha;
}
/**
* Set maximum drag scroll speed in positions/second. Only applies
* if using default ScrollSpeedProfile.
*
* @param max Maximum scroll speed.
*/
public void setMaxScrollSpeed(float max) {
mMaxScrollSpeed = max;
}
/**
* For each DragSortListView Listener interface implemented by
* <code>adapter</code>, this method calls the appropriate
* set*Listener method with <code>adapter</code> as the argument.
*
* @param adapter The ListAdapter providing data to back
* DragSortListView.
*
* @see android.widget.ListView#setAdapter(android.widget.ListAdapter)
*/
@Override
public void setAdapter(ListAdapter adapter) {
mAdapterWrapper = new AdapterWrapper(adapter);
if (adapter != null) {
adapter.registerDataSetObserver(mObserver);
if (adapter instanceof DropListener) {
setDropListener((DropListener) adapter);
}
if (adapter instanceof DragListener) {
setDragListener((DragListener) adapter);
}
if (adapter instanceof RemoveListener) {
setRemoveListener((RemoveListener) adapter);
}
}
super.setAdapter(mAdapterWrapper);
}
/**
* As opposed to {@link ListView#getAdapter()}, which returns
* a heavily wrapped ListAdapter (DragSortListView wraps the
* input ListAdapter {\emph and} ListView wraps the wrapped one).
*
* @return The ListAdapter set as the argument of {@link setAdapter()}
*/
public ListAdapter getInputAdapter() {
if (mAdapterWrapper == null) {
return null;
} else {
return mAdapterWrapper.getAdapter();
}
}
private class AdapterWrapper extends HeaderViewListAdapter {
private ListAdapter mAdapter;
public AdapterWrapper(ListAdapter adapter) {
super(null, null, adapter);
mAdapter = adapter;
}
public ListAdapter getAdapter() {
return mAdapter;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
DragSortItemView v;
View child;
//Log.d("mobeta", "getView: position="+position+" convertView="+convertView);
if (convertView != null) {
v = (DragSortItemView) convertView;
View oldChild = v.getChildAt(0);
child = mAdapter.getView(position, oldChild, v);
if (child != oldChild) {
// shouldn't get here if user is reusing convertViews properly
v.removeViewAt(0);
v.addView(child);
}
} else {
v = new DragSortItemView(getContext());
v.setLayoutParams(new AbsListView.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
child = mAdapter.getView(position, null, v);
v.addView(child);
}
// Set the correct item height given drag state; passed
// View needs to be measured if measurement is required.
adjustItem(position + getHeaderViewsCount(), v, true);
return v;
}
}
private void drawDivider(int expPosition, Canvas canvas) {
final Drawable divider = getDivider();
final int dividerHeight = getDividerHeight();
//Log.d("mobeta", "div="+divider+" divH="+dividerHeight);
if (divider != null && dividerHeight != 0) {
final ViewGroup expItem = (ViewGroup) getChildAt(expPosition - getFirstVisiblePosition());
if (expItem != null) {
final int l = getPaddingLeft();
final int r = getWidth() - getPaddingRight();
final int t;
final int b;
final int childHeight = expItem.getChildAt(0).getHeight();
if (expPosition > mSrcPos) {
t = expItem.getTop() + childHeight;
b = t + dividerHeight;
} else {
b = expItem.getBottom() - childHeight;
t = b - dividerHeight;
}
//Log.d("mobeta", "l="+l+" t="+t+" r="+r+" b="+b);
// Have to clip to support ColorDrawable on <= Gingerbread
canvas.save();
canvas.clipRect(l, t, r, b);
divider.setBounds(l, t, r, b);
divider.draw(canvas);
canvas.restore();
}
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mDragState != IDLE) {
// draw the divider over the expanded item
if (mFirstExpPos != mSrcPos) {
drawDivider(mFirstExpPos, canvas);
}
if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) {
drawDivider(mSecondExpPos, canvas);
}
}
if (mFloatView != null) {
// draw the float view over everything
final int w = mFloatView.getWidth();
final int h = mFloatView.getHeight();
final int alpha = (int) (255f * mCurrFloatAlpha);
canvas.save();
//Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds());
canvas.translate(mFloatLoc.x, mFloatLoc.y);
canvas.clipRect(0, 0, w, h);
//Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds());
canvas.saveLayerAlpha(0, 0, w, h, alpha, Canvas.ALL_SAVE_FLAG);
mFloatView.draw(canvas);
canvas.restore();
canvas.restore();
}
}
private int getItemHeight(int position) {
View v = getChildAt(position - getFirstVisiblePosition());
if (v != null) {
// item is onscreen, just get the height of the View
return v.getHeight();
} else {
// item is offscreen. get child height and calculate
// item height based on current shuffle state
return calcItemHeight(position, getChildHeight(position));
}
}
private void printPosData() {
Log.d("mobeta", "mSrcPos="+mSrcPos+" mFirstExpPos="+mFirstExpPos+" mSecondExpPos="+mSecondExpPos);
}
private class HeightCache {
private SparseIntArray mMap;
private ArrayList<Integer> mOrder;
private int mMaxSize;
public HeightCache(int size) {
mMap = new SparseIntArray(size);
mOrder = new ArrayList<Integer>(size);
mMaxSize = size;
}
/**
* Add item height at position if doesn't already exist.
*/
public void add(int position, int height) {
int currHeight = mMap.get(position, -1);
if (currHeight != height) {
if (currHeight == -1) {
if (mMap.size() == mMaxSize) {
// remove oldest entry
mMap.delete(mOrder.remove(0));
}
} else {
// move position to newest slot
mOrder.remove((Integer) position);
}
mMap.put(position, height);
mOrder.add(position);
}
}
public int get(int position) {
return mMap.get(position, -1);
}
public void clear() {
mMap.clear();
mOrder.clear();
}
}
/**
* Get the shuffle edge for item at position when top of
* item is at y-coord top. Assumes that current item heights
* are consistent with current float view location and
* thus expanded positions and slide fraction. i.e. Should not be
* called between update of expanded positions/slide fraction
* and layoutChildren.
*
* @param position
* @param top
* @param height Height of item at position. If -1, this function
* calculates this height.
*
* @return Shuffle line between position-1 and position (for
* the given view of the list; that is, for when top of item at
* position has y-coord of given `top`). If
* floating View (treated as horizontal line) is dropped
* immediately above this line, it lands in position-1. If
* dropped immediately below this line, it lands in position.
*/
private int getShuffleEdge(int position, int top) {
final int numHeaders = getHeaderViewsCount();
final int numFooters = getFooterViewsCount();
// shuffle edges are defined between items that can be
// dragged; there are N-1 of them if there are N draggable
// items.
if (position <= numHeaders || (position >= getCount() - numFooters)) {
return top;
}
int divHeight = getDividerHeight();
int edge;
int maxBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
int childHeight = getChildHeight(position);
int itemHeight = getItemHeight(position);
// first calculate top of item given that floating View is
// centered over src position
int otop = top;
if (mSecondExpPos <= mSrcPos) {
// items are expanded on and/or above the source position
if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) {
if (position == mSrcPos) {
otop = top + itemHeight - mFloatViewHeight;
} else {
int blankHeight = itemHeight - childHeight;
otop = top + blankHeight - maxBlankHeight;
}
} else if (position > mSecondExpPos && position <= mSrcPos) {
otop = top - maxBlankHeight;
}
} else {
// items are expanded on and/or below the source position
if (position > mSrcPos && position <= mFirstExpPos) {
otop = top + maxBlankHeight;
} else if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) {
int blankHeight = itemHeight - childHeight;
otop = top + blankHeight;
}
}
// otop is set
if (position <= mSrcPos) {
edge = otop + (mFloatViewHeight - divHeight - getChildHeight(position - 1)) / 2;
} else {
edge = otop + (childHeight - divHeight - mFloatViewHeight) / 2;
}
return edge;
}
private boolean updatePositions() {
final int first = getFirstVisiblePosition();
int startPos = mFirstExpPos;
View startView = getChildAt(startPos - first);
if (startView == null) {
startPos = first + getChildCount() / 2;
startView = getChildAt(startPos - first);
}
int startTop = startView.getTop();
int itemHeight = startView.getHeight();
int edge = getShuffleEdge(startPos, startTop);
int lastEdge = edge;
int divHeight = getDividerHeight();
//Log.d("mobeta", "float mid="+mFloatViewMid);
int itemPos = startPos;
int itemTop = startTop;
if (mFloatViewMid < edge) {
// scanning up for float position
//Log.d("mobeta", " edge="+edge);
while (itemPos >= 0) {
itemPos--;
itemHeight = getItemHeight(itemPos);
if (itemPos == 0) {
edge = itemTop - divHeight - itemHeight;
break;
}
itemTop -= itemHeight + divHeight;
edge = getShuffleEdge(itemPos, itemTop);
//Log.d("mobeta", " edge="+edge);
if (mFloatViewMid >= edge) {
break;
}
lastEdge = edge;
}
} else {
// scanning down for float position
//Log.d("mobeta", " edge="+edge);
final int count = getCount();
while (itemPos < count) {
if (itemPos == count - 1) {
edge = itemTop + divHeight + itemHeight;
break;
}
itemTop += divHeight + itemHeight;
itemHeight = getItemHeight(itemPos + 1);
edge = getShuffleEdge(itemPos + 1, itemTop);
//Log.d("mobeta", " edge="+edge);
// test for hit
if (mFloatViewMid < edge) {
break;
}
lastEdge = edge;
itemPos++;
}
}
final int numHeaders = getHeaderViewsCount();
final int numFooters = getFooterViewsCount();
boolean updated = false;
int oldFirstExpPos = mFirstExpPos;
int oldSecondExpPos = mSecondExpPos;
float oldSlideFrac = mSlideFrac;
if (mAnimate) {
int edgeToEdge = Math.abs(edge - lastEdge);
int edgeTop, edgeBottom;
if (mFloatViewMid < edge) {
edgeBottom = edge;
edgeTop = lastEdge;
} else {
edgeTop = edge;
edgeBottom = lastEdge;
}
//Log.d("mobeta", "edgeTop="+edgeTop+" edgeBot="+edgeBottom);
int slideRgnHeight = (int) (0.5f * mSlideRegionFrac * edgeToEdge);
float slideRgnHeightF = (float) slideRgnHeight;
int slideEdgeTop = edgeTop + slideRgnHeight;
int slideEdgeBottom = edgeBottom - slideRgnHeight;
// Three regions
if (mFloatViewMid < slideEdgeTop) {
mFirstExpPos = itemPos - 1;
mSecondExpPos = itemPos;
mSlideFrac = 0.5f * ((float) (slideEdgeTop - mFloatViewMid)) / slideRgnHeightF;
//Log.d("mobeta", "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac);
} else if (mFloatViewMid < slideEdgeBottom) {
mFirstExpPos = itemPos;
mSecondExpPos = itemPos;
} else {
mFirstExpPos = itemPos;
mSecondExpPos = itemPos + 1;
mSlideFrac = 0.5f * (1.0f + ((float) (edgeBottom - mFloatViewMid)) / slideRgnHeightF);
//Log.d("mobeta", "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac);
}
} else {
mFirstExpPos = itemPos;
mSecondExpPos = itemPos;
}
// correct for headers and footers
if (mFirstExpPos < numHeaders) {
itemPos = numHeaders;
mFirstExpPos = itemPos;
mSecondExpPos = itemPos;
} else if (mSecondExpPos >= getCount() - numFooters) {
itemPos = getCount() - numFooters - 1;
mFirstExpPos = itemPos;
mSecondExpPos = itemPos;
}
if (mFirstExpPos != oldFirstExpPos || mSecondExpPos != oldSecondExpPos || mSlideFrac != oldSlideFrac) {
updated = true;
}
if (itemPos != mFloatPos) {
if (mDragListener != null) {
mDragListener.drag(mFloatPos - numHeaders, itemPos - numHeaders);
}
mFloatPos = itemPos;
updated = true;
}
return updated;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mTrackDragSort) {
mDragSortTracker.appendState();
}
}
private class SmoothAnimator implements Runnable {
private long mStartTime;
private float mDurationF;
private float mAlpha;
private float mA, mB, mC, mD;
private boolean mCanceled;
public SmoothAnimator(float smoothness, int duration) {
mAlpha = smoothness;
mDurationF = (float) duration;
mA = mD = 1f / (2f * mAlpha * (1f - mAlpha));
mB = mAlpha / (2f * (mAlpha - 1f));
mC = 1f / (1f - mAlpha);
}
public float transform(float frac) {
if (frac < mAlpha) {
return mA * frac * frac;
} else if (frac < 1f - mAlpha) {
return mB + mC * frac;
} else {
return 1f - mD * (frac - 1f) * (frac - 1f);
}
}
public void start() {
mStartTime = SystemClock.uptimeMillis();
mCanceled = false;
onStart();
post(this);
}
public void cancel() {
mCanceled = true;
}
public void onStart() {
//stub
}
public void onUpdate(float frac, float smoothFrac) {
//stub
}
public void onStop() {
//stub
}
@Override
public void run() {
if (mCanceled) {
return;
}
float fraction = ((float) (SystemClock.uptimeMillis() - mStartTime)) / mDurationF;
if (fraction >= 1f) {
onUpdate(1f, 1f);
onStop();
} else {
onUpdate(fraction, transform(fraction));
post(this);
}
}
}
/**
* Centers floating View under touch point.
*/
private class LiftAnimator extends SmoothAnimator {
private float mInitDragDeltaY;
private float mFinalDragDeltaY;
public LiftAnimator(float smoothness, int duration) {
super(smoothness, duration);
}
@Override
public void onStart() {
mInitDragDeltaY = mDragDeltaY;
mFinalDragDeltaY = mFloatViewHeightHalf;
}
@Override
public void onUpdate(float frac, float smoothFrac) {
if (mDragState != DRAGGING) {
cancel();
} else {
mDragDeltaY = (int) (smoothFrac * mFinalDragDeltaY + (1f - smoothFrac) * mInitDragDeltaY);
mFloatLoc.y = mY - mDragDeltaY;
doDragFloatView(true);
}
}
}
/**
* Centers floating View over drop slot before destroying.
*/
private class DropAnimator extends SmoothAnimator {
private int mDropPos;
private int srcPos;
private float mInitDeltaY;
private float mInitDeltaX;
public DropAnimator(float smoothness, int duration) {
super(smoothness, duration);
}
@Override
public void onStart() {
mDropPos = mFloatPos;
srcPos = mSrcPos;
mDragState = DROPPING;
mInitDeltaY = mFloatLoc.y - getTargetY();
mInitDeltaX = mFloatLoc.x - getPaddingLeft();
}
private int getTargetY() {
final int first = getFirstVisiblePosition();
final int otherAdjust = (mItemHeightCollapsed + getDividerHeight()) / 2;
View v = getChildAt(mDropPos - first);
int targetY = -1;
if (v != null) {
if (mDropPos == srcPos) {
targetY = v.getTop();
} else if (mDropPos < srcPos) {
// expanded down
targetY = v.getTop() - otherAdjust;
} else {
// expanded up
targetY = v.getBottom() + otherAdjust - mFloatViewHeight;
}
} else {
// drop position is not on screen?? no animation
cancel();
}
return targetY;
}
@Override
public void onUpdate(float frac, float smoothFrac) {
final int targetY = getTargetY();
final float deltaY = mFloatLoc.y - targetY;
final float f = 1f - smoothFrac;
if (f < Math.abs(deltaY / mInitDeltaY)) {
mFloatLoc.y = targetY + (int) (mInitDeltaY * f);
mFloatLoc.x = getPaddingLeft() + (int) (mInitDeltaX * f);
doDragFloatView(true);
}
}
@Override
public void onStop() {
dropFloatView();
}
}
/**
* Collapses expanded items.
*/
private class RemoveAnimator extends SmoothAnimator {
private float mFirstStartBlank;
private float mSecondStartBlank;
private int mFirstChildHeight = -1;
private int mSecondChildHeight = -1;
private int mFirstPos;
private int mSecondPos;
private int srcPos;
public RemoveAnimator(float smoothness, int duration) {
super(smoothness, duration);
}
@Override
public void onStart() {
mFirstChildHeight = -1;
mSecondChildHeight = -1;
mFirstPos = mFirstExpPos;
mSecondPos = mSecondExpPos;
srcPos = mSrcPos;
mDragState = REMOVING;
destroyFloatView();
}
@Override
public void onUpdate(float frac, float smoothFrac) {
float f = 1f - smoothFrac;
final int firstVis = getFirstVisiblePosition();
View item = getChildAt(mFirstPos - firstVis);
ViewGroup.LayoutParams lp;
int blank;
if (item != null) {
if (mFirstChildHeight == -1) {
mFirstChildHeight = getChildHeight(mFirstPos, item, false);
mFirstStartBlank = (float) (item.getHeight() - mFirstChildHeight);
}
blank = Math.max((int) (f * mFirstStartBlank), 1);
lp = item.getLayoutParams();
lp.height = mFirstChildHeight + blank;
item.setLayoutParams(lp);
}
if (mSecondPos != mFirstPos) {
item = getChildAt(mSecondPos - firstVis);
if (item != null) {
if (mSecondChildHeight == -1) {
mSecondChildHeight = getChildHeight(mSecondPos, item, false);
mSecondStartBlank = (float) (item.getHeight() - mSecondChildHeight);
}
blank = Math.max((int) (f * mSecondStartBlank), 1);
lp = item.getLayoutParams();
lp.height = mSecondChildHeight + blank;
item.setLayoutParams(lp);
}
}
}
@Override
public void onStop() {
doRemoveItem();
}
}
/**
* Removes an item from the list and animates the removal.
*
* @param which Position to remove (NOTE: headers/footers ignored!
* this is a position in your input ListAdapter).
*/
public void removeItem(int which) {
if (mDragState == IDLE || mDragState == DRAGGING) {
if (mDragState == IDLE) {
// called from outside drag-sort
mSrcPos = getHeaderViewsCount() + which;
mFirstExpPos = mSrcPos;
mSecondExpPos = mSrcPos;
mFloatPos = mSrcPos;
View v = getChildAt(mSrcPos - getFirstVisiblePosition());
if (v != null) {
v.setVisibility(View.INVISIBLE);
}
}
if (mInTouchEvent) {
switch (mCancelMethod) {
case ON_TOUCH_EVENT:
super.onTouchEvent(mCancelEvent);
break;
case ON_INTERCEPT_TOUCH_EVENT:
super.onInterceptTouchEvent(mCancelEvent);
break;
}
}
if (mRemoveAnimator != null) {
mRemoveAnimator.start();
} else {
doRemoveItem(which);
}
}
}
/**
* Move an item, bypassing the drag-sort process. Simply calls
* through to {@link DropListener#drop(int, int)}.
*
* @param from Position to move (NOTE: headers/footers ignored!
* this is a position in your input ListAdapter).
* @param to Target position (NOTE: headers/footers ignored!
* this is a position in your input ListAdapter).
*/
public void moveItem(int from, int to) {
if (mDropListener != null) {
final int count = getInputAdapter().getCount();
if (from >= 0 && from < count && to >= 0 && to < count) {
mDropListener.drop(from, to);
}
}
}
/**
* Cancel a drag. Calls {@link #stopDrag(boolean, boolean)} with
* <code>true</code> as the first argument.
*/
public void cancelDrag() {
if (mDragState == DRAGGING) {
mDragScroller.stopScrolling(true);
destroyFloatView();
clearPositions();
adjustAllItems();
if (mInTouchEvent) {
mDragState = STOPPED;
} else {
mDragState = IDLE;
}
}
}
private void clearPositions() {
mSrcPos = -1;
mFirstExpPos = -1;
mSecondExpPos = -1;
mFloatPos = -1;
}
private void dropFloatView() {
// must set to avoid cancelDrag being called from the
// DataSetObserver
mDragState = DROPPING;
if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) {
final int numHeaders = getHeaderViewsCount();
mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders);
}
destroyFloatView();
adjustOnReorder();
clearPositions();
adjustAllItems();
// now the drag is done
if (mInTouchEvent) {
mDragState = STOPPED;
} else {
mDragState = IDLE;
}
}
private void doRemoveItem() {
doRemoveItem(mSrcPos - getHeaderViewsCount());
}
/**
* Removes dragged item from the list. Calls RemoveListener.
*/
private void doRemoveItem(int which) {
// must set to avoid cancelDrag being called from the
// DataSetObserver
mDragState = REMOVING;
// end it
if (mRemoveListener != null) {
mRemoveListener.remove(which);
}
destroyFloatView();
adjustOnReorder();
clearPositions();
// now the drag is done
if (mInTouchEvent) {
mDragState = STOPPED;
} else {
mDragState = IDLE;
}
}
private void adjustOnReorder() {
final int firstPos = getFirstVisiblePosition();
//Log.d("mobeta", "first="+firstPos+" src="+mSrcPos);
if (mSrcPos < firstPos) {
// collapsed src item is off screen;
// adjust the scroll after item heights have been fixed
View v = getChildAt(0);
int top = 0;
if (v != null) {
top = v.getTop();
}
//Log.d("mobeta", "top="+top+" fvh="+mFloatViewHeight);
setSelectionFromTop(firstPos - 1, top - getPaddingTop());
}
}
/**
* Stop a drag in progress. Pass <code>true</code> if you would
* like to remove the dragged item from the list.
*
* @param remove Remove the dragged item from the list. Calls
* a registered RemoveListener, if one exists. Otherwise, calls
* the DropListener, if one exists.
*
* @return True if the stop was successful. False if there is
* no floating View.
*/
public boolean stopDrag(boolean remove) {
if (mFloatView != null) {
mDragScroller.stopScrolling(true);
if (remove) {
removeItem(mSrcPos - getHeaderViewsCount());
} else {
if (mDropAnimator != null) {
mDropAnimator.start();
} else {
dropFloatView();
}
}
if (mTrackDragSort) {
mDragSortTracker.stopTracking();
}
return true;
} else {
// stop failed
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mIgnoreTouchEvent) {
mIgnoreTouchEvent = false;
return false;
}
if (!mDragEnabled) {
return super.onTouchEvent(ev);
}
boolean more = false;
boolean lastCallWasIntercept = mLastCallWasIntercept;
mLastCallWasIntercept = false;
if (!lastCallWasIntercept) {
saveTouchCoords(ev);
}
//if (mFloatView != null) {
if (mDragState == DRAGGING) {
onDragTouchEvent(ev);
more = true; //give us more!
} else {
// what if float view is null b/c we dropped in middle
// of drag touch event?
//if (mDragState != STOPPED) {
if (mDragState == IDLE) {
if (super.onTouchEvent(ev)) {
more = true;
}
}
int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
doActionUpOrCancel();
break;
default:
if (more) {
mCancelMethod = ON_TOUCH_EVENT;
}
}
}
return more;
}
private void doActionUpOrCancel() {
mCancelMethod = NO_CANCEL;
mInTouchEvent = false;
if (mDragState == STOPPED) {
mDragState = IDLE;
}
mCurrFloatAlpha = mFloatAlpha;
mChildHeightCache.clear();
}
private void saveTouchCoords(MotionEvent ev) {
int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action != MotionEvent.ACTION_DOWN) {
mLastX = mX;
mLastY = mY;
}
mX = (int) ev.getX();
mY = (int) ev.getY();
if (action == MotionEvent.ACTION_DOWN) {
mLastX = mX;
mLastY = mY;
}
mOffsetX = (int) ev.getRawX() - mX;
mOffsetY = (int) ev.getRawY() - mY;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!mDragEnabled) {
return super.onInterceptTouchEvent(ev);
}
saveTouchCoords(ev);
mLastCallWasIntercept = true;
int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_DOWN) {
if (mDragState != IDLE) {
// intercept and ignore
mIgnoreTouchEvent = true;
return true;
}
mInTouchEvent = true;
}
boolean intercept = false;
// the following deals with calls to super.onInterceptTouchEvent
if (mFloatView != null) {
// super's touch event canceled in startDrag
intercept = true;
} else {
if (super.onInterceptTouchEvent(ev)) {
intercept = true;
}
switch (action) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
doActionUpOrCancel();
break;
default:
if (intercept) {
mCancelMethod = ON_TOUCH_EVENT;
} else {
mCancelMethod = ON_INTERCEPT_TOUCH_EVENT;
}
}
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
mInTouchEvent = false;
}
return intercept;
}
/**
* Set the width of each drag scroll region by specifying
* a fraction of the ListView height.
*
* @param heightFraction Fraction of ListView height. Capped at
* 0.5f.
*
*/
public void setDragScrollStart(float heightFraction) {
setDragScrollStarts(heightFraction, heightFraction);
}
/**
* Set the width of each drag scroll region by specifying
* a fraction of the ListView height.
*
* @param upperFrac Fraction of ListView height for up-scroll bound.
* Capped at 0.5f.
* @param lowerFrac Fraction of ListView height for down-scroll bound.
* Capped at 0.5f.
*
*/
public void setDragScrollStarts(float upperFrac, float lowerFrac) {
if (lowerFrac > 0.5f) {
mDragDownScrollStartFrac = 0.5f;
} else {
mDragDownScrollStartFrac = lowerFrac;
}
if (upperFrac > 0.5f) {
mDragUpScrollStartFrac = 0.5f;
} else {
mDragUpScrollStartFrac = upperFrac;
}
if (getHeight() != 0) {
updateScrollStarts();
}
}
private void continueDrag(int x, int y) {
// proposed position
mFloatLoc.x = x - mDragDeltaX;
mFloatLoc.y = y - mDragDeltaY;
doDragFloatView(true);
int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf);
int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf);
// get the current scroll direction
int currentScrollDir = mDragScroller.getScrollDir();
if (minY > mLastY && minY > mDownScrollStartY && currentScrollDir != DragScroller.DOWN) {
// dragged down, it is below the down scroll start and it is not scrolling up
if (currentScrollDir != DragScroller.STOP) {
// moved directly from up scroll to down scroll
mDragScroller.stopScrolling(true);
}
// start scrolling down
mDragScroller.startScrolling(DragScroller.DOWN);
} else if (maxY < mLastY && maxY < mUpScrollStartY && currentScrollDir != DragScroller.UP) {
// dragged up, it is above the up scroll start and it is not scrolling up
if (currentScrollDir != DragScroller.STOP) {
// moved directly from down scroll to up scroll
mDragScroller.stopScrolling(true);
}
// start scrolling up
mDragScroller.startScrolling(DragScroller.UP);
}
else if (maxY >= mUpScrollStartY && minY <= mDownScrollStartY && mDragScroller.isScrolling()) {
// not in the upper nor in the lower drag-scroll regions but it is still scrolling
mDragScroller.stopScrolling(true);
}
}
private void updateScrollStarts() {
final int padTop = getPaddingTop();
final int listHeight = getHeight() - padTop - getPaddingBottom();
float heightF = (float) listHeight;
mUpScrollStartYF = padTop + mDragUpScrollStartFrac * heightF;
mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac) * heightF;
mUpScrollStartY = (int) mUpScrollStartYF;
mDownScrollStartY = (int) mDownScrollStartYF;
mDragUpScrollHeight = mUpScrollStartYF - padTop;
mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
updateScrollStarts();
}
private void adjustAllItems() {
final int first = getFirstVisiblePosition();
final int last = getLastVisiblePosition();
int begin = Math.max(0, getHeaderViewsCount() - first);
int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount() - first);
for (int i = begin; i <= end; ++i) {
View v = getChildAt(i);
if (v != null) {
adjustItem(first + i, v, false);
}
}
}
private void adjustItem(int position) {
View v = getChildAt(position - getFirstVisiblePosition());
if (v != null) {
adjustItem(position, v, false);
}
}
/**
* Sets layout param height, gravity, and visibility on
* wrapped item.
*/
private void adjustItem(int position, View v, boolean invalidChildHeight) {
// Adjust item height
int height = calcItemHeight(position, v, invalidChildHeight);
ViewGroup.LayoutParams lp = v.getLayoutParams();
if (height != lp.height) {
lp.height = height;
v.setLayoutParams(lp);
}
// Adjust item gravity
if (position == mFirstExpPos || position == mSecondExpPos) {
if (position < mSrcPos) {
((DragSortItemView) v).setGravity(Gravity.BOTTOM);
} else if (position > mSrcPos) {
((DragSortItemView) v).setGravity(Gravity.TOP);
}
}
// Finally adjust item visibility
int oldVis = v.getVisibility();
int vis = View.VISIBLE;
if (position == mSrcPos && mFloatView != null) {
vis = View.INVISIBLE;
}
if (vis != oldVis) {
v.setVisibility(vis);
}
}
private int getChildHeight(int position) {
if (position == mSrcPos) {
return 0;
}
View v = getChildAt(position - getFirstVisiblePosition());
if (v != null) {
// item is onscreen, therefore child height is valid,
// hence the "true"
return getChildHeight(position, v, false);
} else {
// item is offscreen
// first check cache for child height at this position
int childHeight = mChildHeightCache.get(position);
if (childHeight != -1) {
//Log.d("mobeta", "found child height in cache!");
return childHeight;
}
final ListAdapter adapter = getAdapter();
int type = adapter.getItemViewType(position);
// There might be a better place for checking for the following
final int typeCount = adapter.getViewTypeCount();
if (typeCount != mSampleViewTypes.length) {
mSampleViewTypes = new View[typeCount];
}
if (type >= 0) {
if (mSampleViewTypes[type] == null) {
v = adapter.getView(position, null, this);
mSampleViewTypes[type] = v;
} else {
v = adapter.getView(position, mSampleViewTypes[type], this);
}
} else {
// type is HEADER_OR_FOOTER or IGNORE
v = adapter.getView(position, null, this);
}
// current child height is invalid, hence "true" below
childHeight = getChildHeight(position, v, true);
// cache it because this could have been expensive
mChildHeightCache.add(position, childHeight);
return childHeight;
}
}
private int getChildHeight(int position, View item, boolean invalidChildHeight) {
if (position == mSrcPos) {
return 0;
}
View child;
if (position < getHeaderViewsCount() || position >= getCount() - getFooterViewsCount()) {
child = item;
} else {
child = ((ViewGroup) item).getChildAt(0);
}
ViewGroup.LayoutParams lp = child.getLayoutParams();
if (lp != null) {
if (lp.height > 0) {
return lp.height;
}
}
int childHeight = child.getHeight();
if (childHeight == 0 || invalidChildHeight) {
measureItem(child);
childHeight = child.getMeasuredHeight();
}
return childHeight;
}
private int calcItemHeight(int position, View item, boolean invalidChildHeight) {
return calcItemHeight(position, getChildHeight(position, item, invalidChildHeight));
}
private int calcItemHeight(int position, int childHeight) {
int divHeight = getDividerHeight();
boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos;
int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
int slideHeight = (int) (mSlideFrac * maxNonSrcBlankHeight);
int height;
if (position == mSrcPos) {
if (mSrcPos == mFirstExpPos) {
if (isSliding) {
height = slideHeight + mItemHeightCollapsed;
} else {
height = mFloatViewHeight;
}
} else if (mSrcPos == mSecondExpPos) {
// if gets here, we know an item is sliding
height = mFloatViewHeight - slideHeight;
} else {
height = mItemHeightCollapsed;
}
} else if (position == mFirstExpPos) {
if (isSliding) {
height = childHeight + slideHeight;
} else {
height = childHeight + maxNonSrcBlankHeight;
}
} else if (position == mSecondExpPos) {
// we know an item is sliding (b/c 2ndPos != 1stPos)
height = childHeight + maxNonSrcBlankHeight - slideHeight;
} else {
height = childHeight;
}
return height;
}
@Override
public void requestLayout() {
if (!mBlockLayoutRequests) {
super.requestLayout();
}
}
private int adjustScroll(int movePos, View moveItem, int oldFirstExpPos, int oldSecondExpPos) {
int adjust = 0;
final int childHeight = getChildHeight(movePos);
int moveHeightBefore = moveItem.getHeight();
int moveHeightAfter = calcItemHeight(movePos, childHeight);
int moveBlankBefore = moveHeightBefore;
int moveBlankAfter = moveHeightAfter;
if (movePos != mSrcPos) {
moveBlankBefore -= childHeight;
moveBlankAfter -= childHeight;
}
int maxBlank = mFloatViewHeight;
if (mSrcPos != mFirstExpPos && mSrcPos != mSecondExpPos) {
maxBlank -= mItemHeightCollapsed;
}
if (movePos <= oldFirstExpPos) {
if (movePos > mFirstExpPos) {
adjust += maxBlank - moveBlankAfter;
}
} else if (movePos == oldSecondExpPos) {
if (movePos <= mFirstExpPos) {
adjust += moveBlankBefore - maxBlank;
} else if (movePos == mSecondExpPos) {
adjust += moveHeightBefore - moveHeightAfter;
} else {
adjust += moveBlankBefore;
}
} else {
if (movePos <= mFirstExpPos) {
adjust -= maxBlank;
} else if (movePos == mSecondExpPos) {
adjust -= moveBlankAfter;
}
}
return adjust;
}
private void measureItem(View item) {
ViewGroup.LayoutParams lp = item.getLayoutParams();
if (lp == null) {
lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft() + getListPaddingRight(), lp.width);
int hspec;
if (lp.height > 0) {
hspec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
} else {
hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
item.measure(wspec, hspec);
}
private void measureFloatView() {
if (mFloatView != null) {
measureItem(mFloatView);
mFloatViewHeight = mFloatView.getMeasuredHeight();
mFloatViewHeightHalf = mFloatViewHeight / 2;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//Log.d("mobeta", "onMeasure called");
if (mFloatView != null) {
if (mFloatView.isLayoutRequested()) {
measureFloatView();
}
mFloatViewOnMeasured = true; //set to false after layout
}
mWidthMeasureSpec = widthMeasureSpec;
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (mFloatView != null) {
if (mFloatView.isLayoutRequested() && !mFloatViewOnMeasured) {
// Have to measure here when usual android measure
// pass is skipped. This happens during a drag-sort
// when layoutChildren is called directly.
measureFloatView();
}
mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight());
mFloatViewOnMeasured = false;
}
}
protected boolean onDragTouchEvent(MotionEvent ev) {
// we are in a drag
int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (ev.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_CANCEL:
if (mDragState == DRAGGING) {
cancelDrag();
}
doActionUpOrCancel();
break;
case MotionEvent.ACTION_UP:
//Log.d("mobeta", "calling stopDrag from onDragTouchEvent");
if (mDragState == DRAGGING) {
stopDrag(false);
}
doActionUpOrCancel();
break;
case MotionEvent.ACTION_MOVE:
continueDrag((int) ev.getX(), (int) ev.getY());
break;
}
return true;
}
private boolean mFloatViewInvalidated = false;
private void invalidateFloatView() {
mFloatViewInvalidated = true;
}
/**
* Start a drag of item at <code>position</code> using the
* registered FloatViewManager. Calls through
* to {@link #startDrag(int,View,int,int,int)} after obtaining
* the floating View from the FloatViewManager.
*
* @param position Item to drag.
* @param dragFlags Flags that restrict some movements of the
* floating View. For example, set <code>dragFlags |=
* ~{@link #DRAG_NEG_X}</code> to allow dragging the floating
* View in all directions except off the screen to the left.
* @param deltaX Offset in x of the touch coordinate from the
* left edge of the floating View (i.e. touch-x minus float View
* left).
* @param deltaY Offset in y of the touch coordinate from the
* top edge of the floating View (i.e. touch-y minus float View
* top).
*
* @return True if the drag was started, false otherwise. This
* <code>startDrag</code> will fail if we are not currently in
* a touch event, there is no registered FloatViewManager,
* or the FloatViewManager returns a null View.
*/
public boolean startDrag(int position, int dragFlags, int deltaX, int deltaY) {
if (!mInTouchEvent || mFloatViewManager == null) {
return false;
}
View v = mFloatViewManager.onCreateFloatView(position);
if (v == null) {
return false;
} else {
return startDrag(position, v, dragFlags, deltaX, deltaY);
}
}
/**
* Start a drag of item at <code>position</code> without using
* a FloatViewManager.
*
* @param position Item to drag.
* @param floatView Floating View.
* @param dragFlags Flags that restrict some movements of the
* floating View. For example, set <code>dragFlags |=
* ~{@link #DRAG_NEG_X}</code> to allow dragging the floating
* View in all directions except off the screen to the left.
* @param deltaX Offset in x of the touch coordinate from the
* left edge of the floating View (i.e. touch-x minus float View
* left).
* @param deltaY Offset in y of the touch coordinate from the
* top edge of the floating View (i.e. touch-y minus float View
* top).
*
* @return True if the drag was started, false otherwise. This
* <code>startDrag</code> will fail if we are not currently in
* a touch event, <code>floatView</code> is null, or there is
* a drag in progress.
*/
public boolean startDrag(int position, View floatView, int dragFlags, int deltaX, int deltaY) {
if (mDragState != IDLE || !mInTouchEvent || mFloatView != null || floatView == null) {
return false;
}
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
int pos = position + getHeaderViewsCount();
mFirstExpPos = pos;
mSecondExpPos = pos;
mSrcPos = pos;
mFloatPos = pos;
//mDragState = dragType;
mDragState = DRAGGING;
mDragFlags = 0;
mDragFlags |= dragFlags;
mFloatView = floatView;
measureFloatView(); //sets mFloatViewHeight
mDragDeltaX = deltaX;
mDragDeltaY = deltaY;
mDragStartY = mY;
//updateFloatView(mX - mDragDeltaX, mY - mDragDeltaY);
mFloatLoc.x = mX - mDragDeltaX;
mFloatLoc.y = mY - mDragDeltaY;
// set src item invisible
final View srcItem = getChildAt(mSrcPos - getFirstVisiblePosition());
if (srcItem != null) {
srcItem.setVisibility(View.INVISIBLE);
}
if (mTrackDragSort) {
mDragSortTracker.startTracking();
}
// once float view is created, events are no longer passed
// to ListView
switch (mCancelMethod) {
case ON_TOUCH_EVENT:
super.onTouchEvent(mCancelEvent);
break;
case ON_INTERCEPT_TOUCH_EVENT:
super.onInterceptTouchEvent(mCancelEvent);
break;
}
requestLayout();
if (mLiftAnimator != null) {
mLiftAnimator.start();
}
return true;
}
private void doDragFloatView(boolean forceInvalidate) {
int movePos = getFirstVisiblePosition() + getChildCount() / 2;
View moveItem = getChildAt(getChildCount() / 2);
if (moveItem == null) {
return;
}
doDragFloatView(movePos, moveItem, forceInvalidate);
}
private void doDragFloatView(int movePos, View moveItem, boolean forceInvalidate) {
mBlockLayoutRequests = true;
updateFloatView();
int oldFirstExpPos = mFirstExpPos;
int oldSecondExpPos = mSecondExpPos;
boolean updated = updatePositions();
if (updated) {
adjustAllItems();
int scroll = adjustScroll(movePos, moveItem, oldFirstExpPos, oldSecondExpPos);
//Log.d("mobeta", " adjust scroll="+scroll);
setSelectionFromTop(movePos, moveItem.getTop() + scroll - getPaddingTop());
layoutChildren();
}
if (updated || forceInvalidate) {
invalidate();
}
mBlockLayoutRequests = false;
}
/**
* Sets float View location based on suggested values and
* constraints set in mDragFlags.
*/
private void updateFloatView() {
if (mFloatViewManager != null) {
mTouchLoc.set(mX, mY);
mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, mTouchLoc);
}
final int floatX = mFloatLoc.x;
final int floatY = mFloatLoc.y;
// restrict x motion
int padLeft = getPaddingLeft();
if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) {
mFloatLoc.x = padLeft;
} else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) {
mFloatLoc.x = padLeft;
}
// keep floating view from going past bottom of last header view
final int numHeaders = getHeaderViewsCount();
final int numFooters = getFooterViewsCount();
final int firstPos = getFirstVisiblePosition();
final int lastPos = getLastVisiblePosition();
//Log.d("mobeta", "nHead="+numHeaders+" nFoot="+numFooters+" first="+firstPos+" last="+lastPos);
int topLimit = getPaddingTop();
if (firstPos < numHeaders) {
topLimit = getChildAt(numHeaders - firstPos - 1).getBottom();
}
if ((mDragFlags & DRAG_NEG_Y) == 0) {
if (firstPos <= mSrcPos) {
topLimit = Math.max(getChildAt(mSrcPos - firstPos).getTop(), topLimit);
}
}
// bottom limit is top of first footer View or
// bottom of last item in list
int bottomLimit = getHeight() - getPaddingBottom();
if (lastPos >= getCount() - numFooters - 1) {
bottomLimit = getChildAt(getCount() - numFooters - 1 - firstPos).getBottom();
}
if ((mDragFlags & DRAG_POS_Y) == 0) {
if (lastPos >= mSrcPos) {
bottomLimit = Math.min(getChildAt(mSrcPos - firstPos).getBottom(), bottomLimit);
}
}
//Log.d("mobeta", "dragView top=" + (y - mDragDeltaY));
//Log.d("mobeta", "limit=" + limit);
//Log.d("mobeta", "mDragDeltaY=" + mDragDeltaY);
if (floatY < topLimit) {
mFloatLoc.y = topLimit;
} else if (floatY + mFloatViewHeight > bottomLimit) {
mFloatLoc.y = bottomLimit - mFloatViewHeight;
}
// get y-midpoint of floating view (constrained to ListView bounds)
mFloatViewMid = mFloatLoc.y + mFloatViewHeightHalf;
}
private void destroyFloatView() {
if (mFloatView != null) {
mFloatView.setVisibility(GONE);
if (mFloatViewManager != null) {
mFloatViewManager.onDestroyFloatView(mFloatView);
}
mFloatView = null;
invalidate();
}
}
/**
* Interface for customization of the floating View appearance
* and dragging behavior. Implement
* your own and pass it to {@link #setFloatViewManager}. If
* your own is not passed, the default {@link SimpleFloatViewManager}
* implementation is used.
*/
public interface FloatViewManager {
/**
* Return the floating View for item at <code>position</code>.
* DragSortListView will measure and layout this View for you,
* so feel free to just inflate it. You can help DSLV by
* setting some {@link ViewGroup.LayoutParams} on this View;
* otherwise it will set some for you (with a width of FILL_PARENT
* and a height of WRAP_CONTENT).
*
* @param position Position of item to drag (NOTE:
* <code>position</code> excludes header Views; thus, if you
* want to call {@link ListView#getChildAt(int)}, you will need
* to add {@link ListView#getHeaderViewsCount()} to the index).
*
* @return The View you wish to display as the floating View.
*/
public View onCreateFloatView(int position);
/**
* Called whenever the floating View is dragged. Float View
* properties can be changed here. Also, the upcoming location
* of the float View can be altered by setting
* <code>location.x</code> and <code>location.y</code>.
*
* @param floatView The floating View.
* @param location The location (top-left; relative to DSLV
* top-left) at which the float
* View would like to appear, given the current touch location
* and the offset provided in {@link DragSortListView#startDrag}.
* @param touch The current touch location (relative to DSLV
* top-left).
* @param pendingScroll
*/
public void onDragFloatView(View floatView, Point location, Point touch);
/**
* Called when the float View is dropped; lets you perform
* any necessary cleanup. The internal DSLV floating View
* reference is set to null immediately after this is called.
*
* @param floatView The floating View passed to
* {@link #onCreateFloatView(int)}.
*/
public void onDestroyFloatView(View floatView);
}
public void setFloatViewManager(FloatViewManager manager) {
mFloatViewManager = manager;
}
public void setDragListener(DragListener l) {
mDragListener = l;
}
/**
* Allows for easy toggling between a DragSortListView
* and a regular old ListView. If enabled, items are
* draggable, where the drag init mode determines how
* items are lifted (see {@link setDragInitMode(int)}).
* If disabled, items cannot be dragged.
*
* @param enabled Set <code>true</code> to enable list
* item dragging
*/
public void setDragEnabled(boolean enabled) {
mDragEnabled = enabled;
}
public boolean isDragEnabled() {
return mDragEnabled;
}
/**
* This better reorder your ListAdapter! DragSortListView does not do this
* for you; doesn't make sense to. Make sure
* {@link BaseAdapter#notifyDataSetChanged()} or something like it is
* called in your implementation.
*
* @param l
*/
public void setDropListener(DropListener l) {
mDropListener = l;
}
/**
* Probably a no-brainer, but make sure that your remove listener
* calls {@link BaseAdapter#notifyDataSetChanged()} or something like it.
* When an item removal occurs, DragSortListView
* relies on a redraw of all the items to recover invisible views
* and such. Strictly speaking, if you remove something, your dataset
* has changed...
*
* @param l
*/
public void setRemoveListener(RemoveListener l) {
mRemoveListener = l;
}
public interface DragListener {
public void drag(int from, int to);
}
/**
* Your implementation of this has to reorder your ListAdapter!
* Make sure to call
* {@link BaseAdapter#notifyDataSetChanged()} or something like it
* in your implementation.
*
* @author heycosmo
*
*/
public interface DropListener {
public void drop(int from, int to);
}
/**
* Make sure to call
* {@link BaseAdapter#notifyDataSetChanged()} or something like it
* in your implementation.
*
* @author heycosmo
*
*/
public interface RemoveListener {
public void remove(int which);
}
public interface DragSortListener extends DropListener, DragListener, RemoveListener {}
public void setDragSortListener(DragSortListener l) {
setDropListener(l);
setDragListener(l);
setRemoveListener(l);
}
/**
* Completely custom scroll speed profile. Default increases linearly
* with position and is constant in time. Create your own by implementing
* {@link DragSortListView.DragScrollProfile}.
*
* @param ssp
*/
public void setDragScrollProfile(DragScrollProfile ssp) {
if (ssp != null) {
mScrollProfile = ssp;
}
}
/**
* Interface for controlling
* scroll speed as a function of touch position and time. Use
* {@link DragSortListView#setDragScrollProfile(DragScrollProfile)} to
* set custom profile.
*
* @author heycosmo
*
*/
public interface DragScrollProfile {
/**
* Return a scroll speed in pixels/millisecond. Always return a
* positive number.
*
* @param w Normalized position in scroll region (i.e. w \in [0,1]).
* Small w typically means slow scrolling.
* @param t Time (in milliseconds) since start of scroll (handy if you
* want scroll acceleration).
* @return Scroll speed at position w and time t in pixels/ms.
*/
float getSpeed(float w, long t);
}
private class DragScroller implements Runnable {
private boolean mAbort;
private long mPrevTime;
private long mCurrTime;
private int dy;
private float dt;
private long tStart;
private int scrollDir;
public final static int STOP = -1;
public final static int UP = 0;
public final static int DOWN = 1;
private float mScrollSpeed; // pixels per ms
private boolean mScrolling = false;
private int mLastHeader;
private int mFirstFooter;
public boolean isScrolling() {
return mScrolling;
}
public int getScrollDir() {
return mScrolling ? scrollDir : STOP;
}
public DragScroller() {}
public void startScrolling(int dir) {
if (!mScrolling) {
//Debug.startMethodTracing("dslv-scroll");
mAbort = false;
mScrolling = true;
tStart = SystemClock.uptimeMillis();
mPrevTime = tStart;
scrollDir = dir;
post(this);
}
}
public void stopScrolling(boolean now) {
if (now) {
DragSortListView.this.removeCallbacks(this);
mScrolling = false;
} else {
mAbort = true;
}
//Debug.stopMethodTracing();
}
@Override
public void run() {
if (mAbort) {
mScrolling = false;
return;
}
//Log.d("mobeta", "scroll");
final int first = getFirstVisiblePosition();
final int last = getLastVisiblePosition();
final int count = getCount();
final int padTop = getPaddingTop();
final int listHeight = getHeight() - padTop - getPaddingBottom();
int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf);
int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf);
if (scrollDir == UP) {
View v = getChildAt(0);
//Log.d("mobeta", "vtop="+v.getTop()+" padtop="+padTop);
if (v == null) {
mScrolling = false;
return;
} else {
if (first == 0 && v.getTop() == padTop) {
mScrolling = false;
return;
}
}
mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY) / mDragUpScrollHeight, mPrevTime);
} else {
View v = getChildAt(last - first);
if (v == null) {
mScrolling = false;
return;
} else {
if (last == count - 1 && v.getBottom() <= listHeight + padTop) {
mScrolling = false;
return;
}
}
mScrollSpeed = -mScrollProfile.getSpeed((minY - mDownScrollStartYF) / mDragDownScrollHeight, mPrevTime);
}
mCurrTime = SystemClock.uptimeMillis();
dt = (float) (mCurrTime - mPrevTime);
// dy is change in View position of a list item; i.e. positive dy
// means user is scrolling up (list item moves down the screen, remember
// y=0 is at top of View).
dy = (int) Math.round(mScrollSpeed * dt);
int movePos;
if (dy >= 0) {
dy = Math.min(listHeight, dy);
movePos = first;
} else {
dy = Math.max(-listHeight, dy);
movePos = last;
}
final View moveItem = getChildAt(movePos - first);
int top = moveItem.getTop() + dy;
if (movePos == 0 && top > padTop) {
top = padTop;
}
// always do scroll
mBlockLayoutRequests = true;
setSelectionFromTop(movePos, top - padTop);
DragSortListView.this.layoutChildren();
invalidate();
mBlockLayoutRequests = false;
// scroll means relative float View movement
doDragFloatView(movePos, moveItem, false);
mPrevTime = mCurrTime;
//Log.d("mobeta", " updated prevTime="+mPrevTime);
post(this);
}
}
private class DragSortTracker {
StringBuilder mBuilder = new StringBuilder();
File mFile;
private int mNumInBuffer = 0;
private int mNumFlushes = 0;
private boolean mTracking = false;
public DragSortTracker() {
File root = Environment.getExternalStorageDirectory();
mFile = new File(root, "dslv_state.txt");
if (!mFile.exists()) {
try {
mFile.createNewFile();
Log.d("mobeta", "file created");
} catch (IOException e) {
Log.w("mobeta", "Could not create dslv_state.txt");
Log.d("mobeta", e.getMessage());
}
}
}
public void startTracking() {
mBuilder.append("<DSLVStates>\n");
mNumFlushes = 0;
mTracking = true;
}
public void appendState() {
if (!mTracking) {
return;
}
mBuilder.append("<DSLVState>\n");
final int children = getChildCount();
final int first = getFirstVisiblePosition();
mBuilder.append(" <Positions>");
for (int i = 0; i < children; ++i) {
mBuilder.append(first + i).append(",");
}
mBuilder.append("</Positions>\n");
mBuilder.append(" <Tops>");
for (int i = 0; i < children; ++i) {
mBuilder.append(getChildAt(i).getTop()).append(",");
}
mBuilder.append("</Tops>\n");
mBuilder.append(" <Bottoms>");
for (int i = 0; i < children; ++i) {
mBuilder.append(getChildAt(i).getBottom()).append(",");
}
mBuilder.append("</Bottoms>\n");
mBuilder.append(" <FirstExpPos>").append(mFirstExpPos).append("</FirstExpPos>\n");
mBuilder.append(" <FirstExpBlankHeight>")
.append(getItemHeight(mFirstExpPos) - getChildHeight(mFirstExpPos))
.append("</FirstExpBlankHeight>\n");
mBuilder.append(" <SecondExpPos>").append(mSecondExpPos).append("</SecondExpPos>\n");
mBuilder.append(" <SecondExpBlankHeight>")
.append(getItemHeight(mSecondExpPos) - getChildHeight(mSecondExpPos))
.append("</SecondExpBlankHeight>\n");
mBuilder.append(" <SrcPos>").append(mSrcPos).append("</SrcPos>\n");
mBuilder.append(" <SrcHeight>").append(mFloatViewHeight + getDividerHeight()).append("</SrcHeight>\n");
mBuilder.append(" <ViewHeight>").append(getHeight()).append("</ViewHeight>\n");
mBuilder.append(" <LastY>").append(mLastY).append("</LastY>\n");
mBuilder.append(" <FloatY>").append(mFloatViewMid).append("</FloatY>\n");
mBuilder.append(" <ShuffleEdges>");
for (int i = 0; i < children; ++i) {
mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(",");
}
mBuilder.append("</ShuffleEdges>\n");
mBuilder.append("</DSLVState>\n");
mNumInBuffer++;
if (mNumInBuffer > 1000) {
flush();
mNumInBuffer = 0;
}
}
public void flush() {
if (!mTracking) {
return;
}
// save to file on sdcard
try {
boolean append = true;
if (mNumFlushes == 0) {
append = false;
}
FileWriter writer = new FileWriter(mFile, append);
writer.write(mBuilder.toString());
mBuilder.delete(0, mBuilder.length());
writer.flush();
writer.close();
mNumFlushes++;
} catch (IOException e) {
// do nothing
}
}
public void stopTracking() {
if (mTracking) {
mBuilder.append("</DSLVStates>\n");
flush();
mTracking = false;
}
}
}
}